package com.furkid.service.service.token;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class TokenService {
    private final StringRedisTemplate srt;

    private static final Duration TOKEN_TTL = Duration.ofMinutes(30);
    private static final Duration SLIDING_THRESHOLD = Duration.ofMinutes(10);

    private String tokenKey(String tid){ return "token:" + tid; }
    private String userTokensKey(long uid){ return "user:" + uid + ":tokens"; }
    private String userTokenKey(long uid){ return "user:" + uid + ":token"; }

    private String accessKey(String tid){ return "access:" + tid; }
    private String refreshKey(String tid){ return "refresh:" + tid; }

    private static final String LUA_ROTATE_REFRESH =
            "if redis.call('EXISTS', KEYS[1]) == 1 then " +
                    "  redis.call('SET', KEYS[2], ARGV[1], 'EX', ARGV[2]); " +
                    "  redis.call('DEL', KEYS[1]); " +
                    "  return 1 " +
                    "else return 0 end";

    /** 签发 access_token（短期，放到 Authorization: Bearer） */
    public String issueAccessToken(long uid, String ua, String ip, Duration ttl){
        String tid = UUID.randomUUID().toString().replace("-", "");
        String payload = String.format(
                "{\"uid\":%d,\"type\":\"access\",\"ua\":\"%s\",\"ip\":\"%s\",\"iat\":%d}",
                uid, esc(ua), esc(ip), System.currentTimeMillis()
        );
        srt.opsForValue().set(accessKey(tid), payload, ttl);
        return tid;
    }

    /** 签发 refresh_token（长期，建议放 HttpOnly Cookie） */
    public String issueRefreshToken(long uid, String ua, String ip, Duration ttl){
        String tid = UUID.randomUUID().toString().replace("-", "");
        String payload = String.format(
                "{\"uid\":%d,\"type\":\"refresh\",\"ua\":\"%s\",\"ip\":\"%s\",\"iat\":%d}",
                uid, esc(ua), esc(ip), System.currentTimeMillis()
        );
        srt.opsForValue().set(refreshKey(tid), payload, ttl);
        return tid;
    }

    public String issueToken(long uid, String scope, String ua, String ip){
        String tid = UUID.randomUUID().toString().replace("-", "");
        String payload = String.format("{\"uid\":%d,\"scope\":\"%s\",\"ua\":\"%s\",\"ip\":\"%s\",\"iat\":%d}",
                uid, scope, esc(ua), esc(ip), System.currentTimeMillis());
        srt.opsForValue().set(tokenKey(tid), payload, TOKEN_TTL);
        srt.opsForSet().add(userTokensKey(uid), tid);
        srt.expire(userTokensKey(uid), TOKEN_TTL);
        return tid;
    }

    public String issueTokenSingleDevice(long uid, String scope, String ua, String ip){
        String newTid = UUID.randomUUID().toString().replace("-", "");
        String payload = String.format("{\"uid\":%d,\"scope\":\"%s\",\"ua\":\"%s\",\"ip\":\"%s\",\"iat\":%d}",
                uid, scope, esc(ua), esc(ip), System.currentTimeMillis());
        String oldTid = srt.opsForValue().get(userTokenKey(uid));
        srt.opsForValue().set(tokenKey(newTid), payload, TOKEN_TTL);
        srt.opsForValue().set(userTokenKey(uid), newTid, TOKEN_TTL);
        if (oldTid != null) {
            srt.delete(tokenKey(oldTid));
        }
        return newTid;
    }

    public Long verifyAndMaybeRefresh(String tid){
        String key = tokenKey(tid);
        String val = srt.opsForValue().get(key);
        if (val == null) {
            return null;
        }
        Long uid = extractUid(val);
        Long ttl = srt.getExpire(key, TimeUnit.SECONDS);
        if (ttl != null && ttl > 0 && ttl < SLIDING_THRESHOLD.toSeconds()){
            srt.expire(key, TOKEN_TTL);
            if (uid != null){
                srt.expire(userTokensKey(uid), TOKEN_TTL);
                srt.expire(userTokenKey(uid), TOKEN_TTL);
            }
        }
        return uid;
    }

    /** 校验 refresh_token（与方案A相同） */
    public Long verifyRefresh(String rt){
        if (rt == null || rt.isBlank()) return null;
        String v = srt.opsForValue().get(refreshKey(rt));
        return extractUid(v);
    }

    /** 原子轮换：成功返回 newRt，失败返回 null */
    public String rotateRefresh(String oldRt, Duration ttl){
        // 先拿 uid（也可以不读，直接让 Lua 只判断 KEY 是否存在）
        String oldVal = srt.opsForValue().get(refreshKey(oldRt));
        Long uid = extractUid(oldVal);
        if (uid == null) return null;

        // 生成新的 rt
        String newRt = UUID.randomUUID().toString().replace("-", "");
        String payload = String.format(
                "{\"uid\":%d,\"type\":\"refresh\",\"ua\":\"%s\",\"ip\":\"%s\",\"iat\":%d}",
                uid, "", "", System.currentTimeMillis()
        );

        // 准备脚本并执行
        org.springframework.data.redis.core.script.DefaultRedisScript<Long> script =
                new org.springframework.data.redis.core.script.DefaultRedisScript<>();
        script.setScriptText(LUA_ROTATE_REFRESH);
        script.setResultType(Long.class);

        java.util.List<String> keys = java.util.Arrays.asList(
                refreshKey(oldRt),          // KEYS[1]
                refreshKey(newRt)           // KEYS[2]
        );
        Long ok = srt.execute(script, keys, payload, String.valueOf(ttl.getSeconds()));

        if (ok != null && ok == 1L) {
            return newRt;
        } else {
            return null; // 旧rt不存在或并发被删/过期
        }
    }

    /** 校验 access_token（短期 token；无滑动续期） */
    public Long verifyAccess(String at){
        if (at == null || at.isBlank()) return null;
        String v = srt.opsForValue().get(accessKey(at)); // access:{tid}
        return extractUid(v);
    }

    /** 删除 access_token */
    public void deleteAccessToken(String at) {
        srt.delete(accessKey(at));
    }

    /** 删除 refresh_token */
    public void deleteRefreshToken(String rt) {
        srt.delete(refreshKey(rt));
    }

    public void logout(String tid){
        String val = srt.opsForValue().get(tokenKey(tid));
        Long uid = extractUid(val);
        srt.delete(tokenKey(tid));
        if (uid != null){
            srt.opsForSet().remove(userTokensKey(uid), tid);
            String k = userTokenKey(uid);
            String cur = srt.opsForValue().get(k);
            if (tid.equals(cur)) srt.delete(k);
        }
    }

    public void logoutAll(long uid){
        Set<String> tids = srt.opsForSet().members(userTokensKey(uid));
        if (tids != null) srt.delete(tids.stream().map(this::tokenKey).toList());
        srt.delete(userTokensKey(uid));
        srt.delete(userTokenKey(uid));
    }

    private static String esc(String s){ return s == null ? "" : s.replace("\"","\\\""); }

    private static Long extractUid(String json){
        if (json == null) return null;
        int i = json.indexOf("\"uid\":");
        if (i < 0) return null;
        int j = json.indexOf(',', i);
        String num = (j>i ? json.substring(i+6, j) : json.substring(i+6)).replaceAll("[^0-9]","");
        try { return Long.parseLong(num); } catch (Exception e){ return null; }
    }
}
